A simple REST service

Here is a rather useless ping server. It accepts GET /test/ping and responds with {"ping": "pong"}.

Start by making sure rhc is in python's path,


In [14]:
import sys
sys.path.append('/opt/rhc')

and importing a couple of components.


In [15]:
import rhc.micro as micro
import rhc.async as async

A REST handler

We'll start by creating a simple REST handler. A handler always has at least one argument, request, which holds data about the incoming HTTP request. In this case, we ignore the request, and simply return our desired result.


In [16]:
def ping(request):
    return {'ping': 'pong'}

We can take a look ping in order to know how to refer to it.


In [17]:
ping


Out[17]:
<function __main__.ping>

A REST server

A server is defined using the SERVER, ROUTE and crud (GET, POST, PUT, DELETE) directives in a micro file. A simple definition follows.


In [18]:
p = micro.load_server([
  'SERVER useless 12345',
    'ROUTE /test/ping$',
      'GET __main__.ping',
])

What's happening here?

Function load_server

The load_server helper function dynamically loads server definitions. In this case, the definition is contained in a list, but could also be loaded from a file by specifying the file's name, or by specifying a dot-separated path to the file in the python code tree.

In a microservice implementation, the server definitions are included in the micro file, or in one of the imported files. This function is included for experimentation and development.

SERVER

The SERVER directive provides a name and a listening port for a service. The socket is started and listens for incoming connections.

All by itself, a SERVER doesn't provide much.

ROUTE

The ROUTE directive watches for a specific HTTP resource on incoming connections. In this case, the resource is the exact string /test/ping.

Even when combined with a SERVER, a ROUTE doesn't provide much.

GET

The GET directive tells micro what REST handler to run if an HTTP GET occurs on the most recently defined ROUTE. In this case, we specify the ping function defined earlier. The handler is dynamically imported when the server is started.

Other HTTP methods, PUT, POST, DELETE, can be used as directives as well.

Making a connection to the server

The useless server is now listening, but we need a way to connect to it. We start by defining a connection to the listening port:


In [19]:
con = async.Connection('http://localhost:12345')

And then doing a GET on the /test/ping resource.


In [20]:
async.wait(con.get('/test/ping'))


{u'ping': u'pong'}

Behind the scenes

The async.wait function is pulling double duty here by running both the server code and the client code until the client code completes. Each network event causes the microservice (here running inside wait) to perform some action in response to the event. We'll look at each action in turn.

connect

When con.get (aka the client) is executed, it starts a connection to localhost:12345, and waits. It doesn't explicitly wait for anything, it just stops processing. There is nothing to do until another network event occurs.

accept

When the SERVER listening on port 12345 receives the connect, it accepts the call and waits.

For the curious: The microservice periodically polls the socket listening on port 12345 to see if it is "readable". If it is readable, that means that another socket is trying to connect. When this happens, the microservice "accepts" the connection, creating a new socket which represents the microservice's side of the connection. TCP will make sure that the client side of the connection is notified that the connection is complete.

send

When the client is connected, it sends a GET /test/ping to the server as an HTTP document and waits.

For the curious: How does the client know it is connected? The microservice is continuously polling all the sockets that it knows about. When it notices that the client socket is "readable", it "wakes the client up" by calling a piece of code that handles connection events. In the case of the get function used above, it immediately sends an HTTP document when a connect event occurs.

server receive

When the server receives the entire HTTP document, it matches it to the ROUTE and GET directives, and calls __main__.ping, which immediately returns the dictionary {'ping': 'pong'}. The server sends the dictionary as a json document in an HTTP response to the client and waits.

For the curious: The microservice started polling the connected socket as soon as the connection was completed in the accept step above. When data arrives on the socket, the socket becomes "readable" which tells the microservice that it's possible to read some data. Data is read and parsed until an entire HTTP document is received.

client receive

When the client receives the entire HTTP document, it indicates to the wait function that it is done. The wait function prints the json document and stops.

Configuring a SERVICE

A SERVICE can be configured with values in a config file. This makes it possible to change some behavior without modifying the micro file or any code.

Take a look at the config associated with the previously defined SERVICE by examining the value returned by the load_service function:


In [21]:
p.config


Out[21]:
server.useless.port=12345
server.useless.is_active=True
server.useless.ssl.is_active=False
server.useless.ssl.keyfile=
server.useless.ssl.certfile=

We can change the listening port, turn the listener off (is_active), and enable ssl.

Making a change

In a microservice, the config would be loaded from a file as part of startup. Here we do it dynamically.

Let's change the port:


In [22]:
p.config._load(['server.useless.port=12346'])
p.config


Out[22]:
server.useless.port=12346
server.useless.is_active=True
server.useless.ssl.is_active=False
server.useless.ssl.keyfile=
server.useless.ssl.certfile=

Restarting the SERVER

This changes the behavior of the SERVICE. We'll stop the old listener first, and run our old /test/ping. The re_start function is a helper function, unlikely to be useful in a production microservice.


In [23]:
micro.re_start(p)
con = async.Connection('http://localhost:12345')
async.wait(con.get('/test/ping'))


---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
<ipython-input-23-43df4b37e586> in <module>()
      1 micro.re_start(p)
      2 con = async.Connection('http://localhost:12345')
----> 3 async.wait(con.get('/test/ping'))

/opt/rhc/rhc/async.py in wait(partial_cb, callback_fn, task_fn)
    140         run(partial_cb(task_cb))
    141     else:
--> 142         run(partial_cb(callback_fn))
    143 
    144 

/opt/rhc/rhc/async.py in run(command, delay, loop)
    651 
    652     while not command.is_done:
--> 653         SERVER.service(delay=delay, max_iterations=loop)
    654         TIMERS.service()
    655 

/opt/rhc/rhc/tcpsocket.pyc in service(self, delay, max_iterations)
    145         iterations = 0
    146         did_anything = False
--> 147         while (self._service(delay)):
    148             did_anything = True
    149             delay = 0

/opt/rhc/rhc/tcpsocket.pyc in _service(self, timeout)
    184         for sock, mask in self._poll.poll(timeout * 1000):
    185             processed = True
--> 186             self._poll_map[sock][0]()
    187 
    188         for callback in self._pending:

/opt/rhc/rhc/tcpsocket.pyc in _on_delayed_connect(self)
    439         else:
    440             self.close_reason = 'failed to connect'
--> 441             self.on_fail()
    442             self.close()
    443 

/opt/rhc/rhc/async.py in on_fail(self)
    452 
    453     def on_fail(self):
--> 454         self.done(self.close_reason, 1)
    455 
    456     def on_http_error(self):

/opt/rhc/rhc/async.py in done(self, result, rc)
    365         self.is_done = True
    366         self.timer.cancel()
--> 367         self.context.callback(rc, result)
    368         if not self.close_reason:
    369             self.close_reason = 'transaction complete'

/opt/rhc/rhc/async.py in cb_fn(rc, result)
    128     def cb_fn(rc, result):
    129         if rc != 0:
--> 130             raise Exception(result)
    131         print result
    132     if callback_fn is None:

Exception: failed to connect

As expected...

The old connection to port 12345 doesn't work anymore since we've changed the SERVER's listening port. A simple change to the connection setup will fix the error:


In [24]:
con = async.Connection('http://localhost:12346')
async.wait(con.get('/test/ping'))


{u'ping': u'pong'}

In [ ]: